Angular處理http是使用rx(Reactive Programming)來實作的,類別的名稱為rxjs
在閱讀此篇之前,建議可以了解一下何謂Reactive Programming,其核心概念為何,這樣會比較容易理解本篇的內容
推薦閱讀:Reactive Programming 簡介與教學(以 RxJS 為例)、官網 ReactiveX
RxJS教學:30 天精通 RxJS
將src/app/hero.service.ts
取得資料的方式改由API取得
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { MessageService } from './message.service';
import { HttpClient, HttpHeaders } from '@angular/common/http'; //加入http類別
@Injectable()
export class HeroService {
private heroesUrl = 'api/heroes';//設定要讀的api的位置
//於constructor增加private http: HttpClient
constructor(private http: HttpClient, private messageService: MessageService) { }
/** 改由api取得資料,註解掉舊程式
getHeroes(): Observable<Hero[]> {
// Todo: send the message _after_ fetching the heroes
this.messageService.add('HeroService: fetched heroes');
return of(HEROES);
}*/
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
}
getHero(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.get<Hero>(url);
}
}
上面程式碼註解地方代表有修改過的地方。
所有的HttpClient方法都返回一個Observable的物件。一般來說,Observable
物件會傳送多次資料給接收者,但http.get
所取得的值為例外,因為http方法是發出要求並收到回應後就不會再有動作,因此它只會傳一次資料給取得這個資料的接收者。並且需要在被subscribe()
後才會啟動動作。http.get
預設接收的格式為json
,會自動對照所取得的json轉化成一個對應屬性的物件以方便取得資料。
如果在取得api時發生網路錯誤或其他問題導致無法順利取得伺服器資料時,可以用下面的方法來偵聽錯誤
首先,導入rxjs/operators
類別
import { catchError, map, tap } from 'rxjs/operators';
接著,使用pipe
方法擴展Observable
並在裡面下達catchError()
來處理錯誤的狀況
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
catchError(this.handleError('getHeroes', []))
);
}
下面則是catchError
的內容
/**
* 處理http發生的錯誤,讓程式可以繼續正確的運作而不產生exception
* @param operation - 失敗的操作,這邊是getHeroes
* @param result - 可不傳入,最後要回傳出去的Observable物件內容,可在裡面塞一些與api連線失敗時要回傳的資料
*/
private handleError<T> (operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
return of(result as T);
};
}
在上面的T是類型參數,在這個例子中,T代表Hero[]
。這可以讓程式在打api失敗時依舊可取得符合應用程式所期望的類型的回傳值。
取得資料時使用get,而修改資料時使用put,其參數意義如下:
/** 更新伺服器上的資料 */
updateHero (hero: Hero): Observable<any> {
// 該HttpClient.put()方法有三個參數:網址、要更新的數據、選項
return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
}
其中httpOptions
較常設定的是headers
,也就是宣告傳去的格式
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
下面是使用updateHero
的範例
save(): void {
this.heroService.updateHero(this.hero)
.subscribe(() => this.goBack());
}
在RxJS中,有兩個角色,Observable
和Subscription
,Observable
負責產生資料,創建後不會馬上啟動,而在_關注(subscribe)後開始啟動_。
新增資料在rxjs裡是使用http.post()
addHero (hero: Hero): Observable {
return this.http.post(this.heroesUrl, hero, httpOptions).pipe(
catchError(this.handleError('addHero'))
);
}
使用addHero
方法的範例如下:
add(name: string): void {
name = name.trim();
if (!name) { return; } //假如輸入的名稱為空白則不處理
this.heroService.addHero({ name } as Hero) //{ name } as Hero > 代表創建一個Hero,並且其name的值為輸入值,id為空白
.subscribe(hero => {
this.heroes.push(hero); // 將所回傳的物件塞回入列表內
});
}
刪除資料我們使用http.delete()
,範例如下
deleteHero (hero: Hero | number): Observable {
const id = typeof hero === 'number' ? hero : hero.id;
const url = `${this.heroesUrl}/${id}`;
return this.http.delete(url, httpOptions);
}
而呼叫deleteHero
的地方則要這樣寫:
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
this.heroService.deleteHero(hero).subscribe();
}
上面我們可以看到在呼叫deleteHero時,即便刪除完成後沒有要多做任何時,仍然需要加上subscribe()
就像上面說過的,所有rxjs的動作都會在有人subscribe後才會呼叫,因此如果忽略subscribe()
,http將不會將刪除請求發送到伺服器!
Observable需要等到有東西subscribe它,才會被執行。
於src/app/hero.service.ts
增加搜索功能
searchHeroes(term: string): Observable<Hero[]> {
if (!term.trim()) {
// 假如沒有傳值則回傳空資料
return of([]);
}
return this.http.get<Hero[]>(`api/heroes/?name=${term}`).pipe(
tap(_ => this.log(`found heroes matching "${term}"`)),
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
創建一個HeroSearchComponent
的class
ng generate component hero-search
修改src/app/hero-search/hero-search.component.html
<div id="search-component">
<h4>Hero Search</h4>
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
<ul class="search-result">
<li *ngFor="let hero of heroes$ | async" >
<a routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
</li>
</ul>
</div>
上面的程式碼中,要注意的是這一行
<li *ngFor="let hero of heroes$ | async" >
如果光使用for迴圈去使用heroes$,Observable不會做任何事,async透過|
這個pipe來自動做subscribe
的動作,我們可以不用再次的透過subscribe()
來讓Observable
被執行
而heroes$則是告知這個for
迴圈操作的對象是一個Observable
而不是一般的值。
接著我們修改src/app/hero-search/hero-search.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { of } from 'rxjs/observable/of';
import {
debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.css' ]
})
export class HeroSearchComponent implements OnInit {
heroes$: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(private heroService: HeroService) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
}
}
上面的程式碼中,heroes
也需要寫為heroes$
並宣告它是一個Observable
private searchTerms = new Subject<string>();
search(term: string): void {
this.searchTerms.next(term);
}
一個Subject
自己本身是observable
,並且接收一個observable
為參數。因此我們可以對Subject
做許多和Observable
相同的動作。
也可以使用next(value)
去傳值至searchTerms
裡,對任何的Observable
物件也都能這樣操作。
在這邊的search()方法與keyup事件綁定,searchTerms會返回一個Observable
的觀察結果。
另外searchTerms的寫法
this.heroes$ = this.searchTerms.pipe(
// 每次擊鍵後等待300毫秒,然後再搜尋他
debounceTime(300),
// 假如與上次的值相同則忽略
distinctUntilChanged(),
// 當term變更時更新搜索結果,它取消並丟棄先前的搜索可見性,只返回最新的可見的搜索服務。
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
注意:使用switchMap()
,每個有資格的鍵事件都可以觸發HttpClient.get()
方法調用。即使在請求之間暫停了300毫秒,也可能有多個HTTP請求在運行,並且可能不會按照發送的順序返回。switchMap()
保留最初的請求順序,同時只返回最近的HTTP方法調用的observable。來自先前搜尋的結果被取消並被丟棄。
不過,取消先前的searchHeroes()
Observable 並不會中止掛起的HTTP請求。不需要的結果在到達應用程序代碼之前就被丟棄了。
最後成品樣子如下:
完整範例檔請見: live example / download example.